package jmine.tec.aom.util;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jmine.tec.aom.annotation.SyntheticProperty;
import jmine.tec.aom.dao.PropertyDAO;
import jmine.tec.aom.domain.AbstractPropertyValue;
import jmine.tec.aom.domain.Propertized;
import jmine.tec.aom.domain.Property;
import jmine.tec.aom.domain.impl.SyntheticPropertyValue;

import org.hibernate.proxy.HibernateProxyHelper;

import bancosys.tec.persist.dao.BeanNotFoundException;

/**
 * Handles both dynamic and synthetic {@link AbstractPropertyValue} given a target {@link Propertized}.
 * 
 * @author jrenaut
 */
public class FullPropertyHandler implements Propertized, Serializable {

    private static final ConcurrentMap<Class<?>, Collection<Method>> METHOD_CACHE = new ConcurrentHashMap<Class<?>, Collection<Method>>();

    private final Set<Property> dynamicProperties;

    private final Set<AbstractPropertyValue<?>> dynamicPropertyValues;

    private transient Map<Property, SyntheticPropertyValue> syntheticPropertyValues;

    private final Propertized target;

    private final Propertized parent;

    private final PropertyDAO propertyDAO;

    private final PropertyValueUtils propertyValueUtils;

    /**
     * @param utils the {@link PropertyValueUtils}
     * @param dao the {@link PropertyDAO}
     * @param target the target {@link Propertized}
     * @param dynamicProperties the {@link Set} of {@link Property} to be managed
     * @param dynamicPropertyValues the {@link Set} of {@link AbstractPropertyValue} to be managed
     */
    public FullPropertyHandler(PropertyValueUtils utils, PropertyDAO dao, Propertized target, Set<Property> dynamicProperties,
            Set<AbstractPropertyValue<?>> dynamicPropertyValues) {
        this(utils, dao, target, null, dynamicProperties, dynamicPropertyValues);
    }

    /**
     * @param utils the {@link PropertyValueUtils}
     * @param dao the {@link PropertyDAO}
     * @param target the target {@link Propertized}
     * @param parent the parent {@link Propertized}
     * @param dynamicPropertyValues the {@link Set} of {@link AbstractPropertyValue} to be managed
     */
    public FullPropertyHandler(PropertyValueUtils utils, PropertyDAO dao, Propertized target, Propertized parent,
            Set<AbstractPropertyValue<?>> dynamicPropertyValues) {
        this(utils, dao, target, parent, new HashSet<Property>(), dynamicPropertyValues);
    }

    /**
     * @param utils the {@link PropertyValueUtils}
     * @param dao the {@link PropertyDAO}
     * @param target the target {@link Propertized}
     * @param parent the parent {@link Propertized}
     * @param dynamicProperties the {@link Set} of {@link Property} to be managed
     * @param dynamicPropertyValues the {@link Set} of {@link AbstractPropertyValue} to be managed
     */
    private FullPropertyHandler(PropertyValueUtils utils, PropertyDAO dao, Propertized target, Propertized parent,
            Set<Property> dynamicProperties, Set<AbstractPropertyValue<?>> dynamicPropertyValues) {
        this.target = target;
        this.parent = parent;
        this.dynamicProperties = dynamicProperties;
        this.dynamicPropertyValues = dynamicPropertyValues;
        this.propertyDAO = dao;
        this.propertyValueUtils = utils;
    }

    /**
     * {@inheritDoc}
     */
    public Set<Property> getProperties() {
        Set<Property> props = this.getDynamicProperties();
        return Collections.unmodifiableSet(props != null ? new HashSet<Property>(props) : new HashSet<Property>());
    }

    /**
     * {@inheritDoc}
     */
    public Set<Property> getRealProperties() {
        HashSet<Property> props = new HashSet<Property>(this.getProperties());
        props.removeAll(this.getSyntheticProperties());
        return props;
    }

    /**
     * {@inheritDoc}
     */
    public Set<Property> getSyntheticProperties() {
        return Collections.unmodifiableSet(this.getSyntheticPropertyValues().keySet());
    }

    /**
     * {@inheritDoc}
     */
    public Set<AbstractPropertyValue<?>> getPropertyValues() {
        Set<AbstractPropertyValue<?>> propValues = new HashSet<AbstractPropertyValue<?>>(this.dynamicPropertyValues);
        Map<Property, SyntheticPropertyValue> synthValues = this.getSyntheticPropertyValues();
        propValues.addAll(synthValues.values());
        return Collections.unmodifiableSet(propValues);
    }

    /**
     * {@inheritDoc}
     */
    public Propertized getParent() {
        return this.parent;
    }

    /**
     * {@inheritDoc}
     */
    public Property getPropertyByName(String name) {
        for (Property prop : this.getDynamicProperties()) {
            if (prop.getName().equalsIgnoreCase(name)) {
                return prop;
            }
        }
        for (Property prop : this.getSyntheticProperties()) {
            if (prop.getName().equalsIgnoreCase(name)) {
                return prop;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public Property getPropertyByMnemonic(String mnemonic) {
        for (Property prop : this.getDynamicProperties()) {
            if (prop.getMnemonic().equalsIgnoreCase(mnemonic)) {
                return prop;
            }
        }
        for (Property prop : this.getSyntheticProperties()) {
            if (prop.getMnemonic().equalsIgnoreCase(mnemonic)) {
                return prop;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public AbstractPropertyValue getPropertyValue(Property property) {
        if (property != null) {
            SyntheticPropertyValue synthValue = this.getSyntheticPropertyValues().get(property);
            if (synthValue != null) {
                return synthValue;
            } else {
                for (AbstractPropertyValue apv : this.dynamicPropertyValues) {
                    if (apv.getProperty().equals(property)) {
                        return apv;
                    }
                }
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void removePropertyValue(AbstractPropertyValue propertyValue) {
        this.dynamicPropertyValues.remove(propertyValue);
    }

    /**
     * {@inheritDoc}
     */
    public void addPropertyValue(AbstractPropertyValue propertyValue) {
        if (this.isSynthetic(propertyValue)) {
            SyntheticPropertyValue synthValue = this.syntheticPropertyValues.get(propertyValue.getProperty());
            if (synthValue != null) {
                synthValue.setValue(propertyValue.getValue());
            }
        } else {
            AbstractPropertyValue currentValue = this.getPropertyValue(propertyValue.getProperty());
            if (currentValue != null) {
                this.dynamicPropertyValues.remove(currentValue);
            }
            this.dynamicPropertyValues.add(propertyValue);
        }
    }

    /**
     * @param propValue an {@link AbstractPropertyValue}
     * @return true if it is defined as a {@link SyntheticProperty}
     */
    private boolean isSynthetic(AbstractPropertyValue<?> propValue) {
        return this.getSyntheticPropertyValues().containsKey(propValue.getProperty());
    }

    /**
     * @return a {@link Set} of {@link Property}
     */
    private Set<Property> getDynamicProperties() {
        if (this.getParent() != null) {
            return this.getParent().getProperties();
        } else {
            return this.dynamicProperties;
        }
    }

    /**
     * @return a {@link Map} of all {@link SyntheticPropertyValue} defined on the target {@link Propertized}
     */
    private Map<Property, SyntheticPropertyValue> getSyntheticPropertyValues() {
        if (this.syntheticPropertyValues == null) {
            this.syntheticPropertyValues = new HashMap<Property, SyntheticPropertyValue>();
            Collection<Method> methods = this.findAnnotatedMethods();
            if (!methods.isEmpty()) {
                for (Method getter : methods) {
                    SyntheticProperty sp = getter.getAnnotation(SyntheticProperty.class);
                    Property prop = this.getProperty(getter, sp);

                    if (prop != null) {
                        SyntheticPropertyValue spv = this.propertyValueUtils.createSyntheticPropertyValue(prop, this.target, getter);
                        this.syntheticPropertyValues.put(spv.getProperty(), spv);
                    }
                }
            }
        }
        return this.syntheticPropertyValues;
    }

    /**
     * @param getter the getter {@link Method}
     * @param sp the {@link SyntheticProperty}
     * @return the defined {@link Property}
     */
    private Property getProperty(Method getter, SyntheticProperty sp) {
        Property prop = this.getPropertyByMnemonic(sp.mnemonic());
        if (prop == null) {
            try {
                prop = this.propertyDAO.findByGroupAndMnemonic(sp.propertyGroup(), sp.mnemonic());
            } catch (BeanNotFoundException e) {
                throw new IllegalStateException("The Property '" + sp.mnemonic() + "' of Property Group '" + sp.propertyGroup()
                        + "' specified as a synthetic property on method '" + getter.getName() + "' of class '"
                        + this.getTargetClass().getName() + "' was not found on the database!");
            }
        }
        return prop;
    }

    /**
     * Finds in the target {@link Propertized} methods annotated with {@link SyntheticProperty}.
     * 
     * @return the collection of annotated {@link Method}
     */
    private Collection<Method> findAnnotatedMethods() {
        Class realClass = this.getTargetClass();

        if (METHOD_CACHE.containsKey(realClass)) {
            return METHOD_CACHE.get(realClass);
        }

        ArrayList<Method> validated = new ArrayList<Method>();
        Method[] methods = realClass.getMethods();
        for (Method method : methods) {
            if (this.validateMethod(method)) {
                validated.add(method);
            }
        }

        METHOD_CACHE.putIfAbsent(realClass, validated);
        return validated;
    }

    /**
     * @return the actual class of the target {@link Propertized}
     */
    private Class<?> getTargetClass() {
        return HibernateProxyHelper.getClassWithoutInitializingProxy(this.target);
    }

    /**
     * @param method a method
     * @return true it represents a valid {@link SyntheticProperty}
     */
    private boolean validateMethod(Method method) {
        return method.isAnnotationPresent(SyntheticProperty.class) && method.getParameterTypes().length == 0
                && method.getReturnType() != void.class && !Modifier.isStatic(method.getModifiers());
    }

}
